<script setup>
	import { ref, getCurrentInstance, reactive } from 'vue';
	import HelloWorld from './components/HelloWorld.vue';
	import MyWorker from './hash.worker.js?worker';
	const { proxy } = getCurrentInstance();
	import axios from 'axios';
	let axiosSource = axios.CancelToken.source();

	const fileInfo = ref(null);
	const chunkSize = ref(2 * 1024 * 1024); // 切片大小
	/**
	 * 进度
	 */
	const progress = ref(0);

	/**
	 * 视频暂停上传信息
	 */
	const fetchNew = reactive({
		arr: [],
		max: 6,
		count: 0,
		total: 0,
		callback: () => {},
	});
	/**
	 * input改变事件监听
	 * @param {File} e 文件信息
	 */
	function uploadFile(e) {
		const file = e.target.files[0];
		// 如果文件大小大于文件分片大小的5倍才使用分片上传
		if (file.size / chunkSize.value < 5) {
			sendFile(file);
			return;
		}
		createFileMd5(file).then(async (fileMd5) => {
			// 先查询服务器是否已有上传完的文件切片
			let { data } = await getUploadedChunks(fileMd5);
			let uploaded = data.data.length ? data.data.map((v) => v.split('-')[1] - 0) : [];

			// 切割文件
			const chunkArr = await cutBlob(fileMd5, file, uploaded);
			// 已上传进度
			progress.value = ((uploaded.length / fileInfo.value.total) * 100).toFixed(2);
			// 开始上传
			sendRequest(chunkArr, 2, chunkMerge, uploaded.length, fileInfo.value.total);
		});
	}
	/**
	 * 创建文件专属MD5
	 * @param {File} file 文件
	 * @returns {Promise<string>} 文件MD5Hash
	 */
	function createFileMd5(file) {
		return new Promise((resolve) => {
			const worker = new MyWorker();
			worker.postMessage({ file, chunkSize: chunkSize.value });
			worker.onmessage = (event) => {
				resolve(event.data);
				// 关闭 Worker
				worker.terminate();
			};
		});
	}
	/**
	 * 文件分割
	 * @param {String} fileHash 文件md5字符串
	 * @param {File} file 文件信息
	 * @param {Array} uploaded 已上传的文件数组
	 */
	function cutBlob(fileHash, file, uploaded) {
		const chunkArr = []; // 所有切片缓存数组
		const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
		const chunkNums = Math.ceil(file.size / chunkSize.value); // 切片总数

		return new Promise((resolve) => {
			let startIndex = '';
			let endIndex = '';
			let contentItem = '';

			for (let i = 0; i < chunkNums; i++) {
				// 如果已上传则跳过
				if (uploaded.includes(i)) continue;

				startIndex = i * chunkSize.value; // 片段起点
				endIndex = (i + 1) * chunkSize.value; // 片段尾点
				endIndex > file.size && (endIndex = file.size);

				// 切割文件
				contentItem = blobSlice.call(file, startIndex, endIndex);

				chunkArr.push({
					index: i,
					chunk: contentItem,
				});
			}
			fileInfo.value = {
				hash: fileHash,
				total: chunkNums,
				name: file.name,
				size: file.size,
			};
			resolve(chunkArr);
		});
	}
	// 请求并发处理
	function sendRequest(arr, max = 6, callback, count = 0, total = 0) {
		let fetchArr = [];

		let toFetch = () => {
			if (!arr.length) {
				return Promise.resolve();
			}

			const chunkItem = arr.shift();

			const it = sendChunk(chunkItem);

			it.then(
				() => {
					// 成功从任务队列中移除
					fetchArr.splice(fetchArr.indexOf(it), 1);
					count++;
				},
				(err) => {
					// 如果失败则重新放入总队列中
					arr.unshift(chunkItem);
					fetchNew.arr = arr;
					fetchNew.max = max;
					fetchNew.callback = callback;
					fetchNew.count = count;
					fetchNew.total = total;
					console.log(err);
				}
			);
			fetchArr.push(it);

			let p = Promise.resolve();
			if (fetchArr.length >= max) {
				p = Promise.race(fetchArr);
			}
			progress.value = ((count / total) * 100).toFixed(2);
			return p.then(() => toFetch());
		};

		toFetch().then(
			() => {
				Promise.all(fetchArr).then(() => {
					progress.value = 100;
					callback();
				});
			},
			(err) => {
				console.log(err);
			}
		);
	}
	/**
	 * 请求已上传文件
	 * @param {string} hash 文件md5值
	 */
	function getUploadedChunks(hash) {
		return proxy.$http({
			url: '/upload/checkSnippet',
			method: 'post',
			data: { hash },
		});
	}
	// 小文件上传
	function sendChunk(item) {
		if (!item) return;
		let formdata = new FormData();
		formdata.append('file', item.chunk);
		formdata.append('index', item.index);
		formdata.append('hash', fileInfo.value.hash);
		// formdata.append("name", fileInfo.name)

		return proxy.$http({
			url: '/upload/snippet',
			method: 'post',
			data: formdata,
			cancelToken: axiosSource.token,
			headers: { 'Content-Type': 'multipart/form-data' },
		});
	}
	/**
	 * 文件上传方法
	 * @param {File} file 上传的文件
	 */
	function sendFile(file) {
		let formdata = new FormData();
		formdata.append('file', file);
		proxy
			.$http({
				url: '/upload/file',
				method: 'post',
				data: formdata,
				headers: { 'Content-Type': 'multipart/form-data' },
			})
			.then(({ data }) => {
				console.log(data, 'upload/file');
			});
	}
	/**
	 * 请求合并
	 */
	function chunkMerge() {
		proxy
			.$http({
				url: '/upload/merge',
				method: 'post',
				data: fileInfo.value,
			})
			.then((res) => {
				console.log(res.data);
			});
	}

	const uploadPauseStart = ref(false);
	/**
	 * 暂停上传
	 */
	function suspendUpoad() {
		uploadPauseStart.value = true;
		axiosSource.cancel('取消请求');
	}
	/**
	 * 继续上传
	 */
	function continueUplad() {
		uploadPauseStart.value = false;
		axiosSource = axios.CancelToken.source();
		sendRequest(fetchNew.arr, fetchNew.max, fetchNew.callback, fetchNew.count, fetchNew.total);
	}

	const img = ref('');
	function getFileSize() {
		proxy
			.$http({
				url: '/upload/size',
				method: 'post',
				data: {
					name: 'codes (1).png',
				},
			})
			.then(async (res) => {
				console.log(res.data);
				const ds = await concurrencyDownload('/upload/getFile', res.data.size, 1024 * 1024);
				console.log(ds);
				const blob = new Blob([ds]);
				const url = URL.createObjectURL(blob);
				img.value = url;
				console.log(img.value);
			});
	}
	async function concurrencyDownload(path, size, chunkSize) {
		let chunkNum = Math.ceil(size / chunkSize);

		const downloadTask = [];
		for (let i = 1; i <= chunkNum; i++) {
			const rangeStart = chunkSize * (i - 1);
			const rangeEnd = chunkSize * i - 1;

			downloadTask.push(
				proxy.$http({
					url: path,
					method: 'post',
					data: {
						name: 'codes (1).png',
					},
					headers: {
						Range: `bytes=${rangeStart}-${rangeEnd > size ? size - 1 : rangeEnd}`,
					},
					responseType: 'arraybuffer',
				})
			);
		}
		const arrayBuffers = await Promise.all(
			downloadTask.map((task) => {
				return task.then((res) => res.data);
			})
		);
		return mergeArrayBuffer(arrayBuffers);
	}

	function mergeArrayBuffer(arrays) {
		let totalLen = 0;
		for (let arr of arrays) {
			totalLen += arr.byteLength;
		}
		let res = new Uint8Array(totalLen);
		let offset = 0;
		for (let arr of arrays) {
			let uint8Arr = new Uint8Array(arr);
			res.set(uint8Arr, offset);
			offset += arr.byteLength;
		}
		return res.buffer;
	}
</script>

<template>
	<div>
		<h2>大文件上传</h2>
		<input type="file" @change="uploadFile" />
		<button class="btn1" @click="suspendUpoad" :disabled="uploadPauseStart">暂停</button>
		<button
			class="btn2"
			@click="continueUplad"
			:disabled="!uploadPauseStart"
			style="margin-left: 15px"
		>
			继续
		</button>

		<progress :value="progress" :max="100" style="width: 100%; height: 40px" />
		<span>已上传{{ progress }}/100%</span>
		<img :src="img" alt="" style="width: 20%" />
		<a :href="img" :download="img">下载</a>
		<button @click="getFileSize">获取文件大小</button>
		<!-- <img src="http://localhost:3000/uploads/codes%20(1).png" alt="" style="width: 50%" /> -->
	</div>
</template>

<style scoped>
	.btn1 {
		background-color: #f56c6c;
	}
	.btn2 {
		background-color: #67c23a;
	}
	button:hover:disabled {
		cursor: no-drop;
	}
</style>
